Il est trs courant que nous ayons besoin de grer des dates en programmation.
Que ce soit pour grer les dates d'anniversaires des clients d'un site web, annoncer un compte rebours ou encore calculer le nombre de jours entre deux dates.
Pour raliser ces oprations, Python dispose de la bibliothque native datetime qui permet de grer des objets reprsentant des dates et des heures.
Vous avez peut-tre dj entendu parler de l'Epoch , une date arbitraire aussi appele heure Unix :
L'heure Unix ou heure Posix (aussi appele Unix Timestamp) est une mesure du temps base sur le nombre de secondes coules depuis le 1er janvier 1970 00:00:00 UTC, hors secondes intercalaires. Elle est utilise principalement dans les systmes qui respectent la norme POSIX1, dont les systmes de type Unix, d'o son nom. C'est la reprsentation POSIX du temps.
dfinition de Wikipedia
Le calcul du temps peut s'avrer complexe pour tout un tas de raison comme les secondes intercalaires, les fuseaux horaires ou encore l'heure d't.
Gnralement cela ne pose pas de problmes (on a rarement besoin de prendre en compte les potentielles microsecondes de diffrence entre le temps universel et le temps solaire).
Mais nous verrons dans la suite de cet article que notamment les fuseaux horaires doivent tre pris en compte pour viter des erreurs qui peuvent s'avrer problmatiques (comme envoyer un mail 3h du matin au lieu de 9).
Avec Python, il est possible d'afficher le nombre de secondes coules depuis l'heure Unix avec la fonction time du module time :
>>> from time import time >>> time() 1634911800.733942
Au moment o j'cris cet article, il s'est donc coul 1634911800 secondes depuis le 1er janvier 1970.
datetimePour aller un peu plus loin et manipuler des dates (et des heures), Python dispose du module datetime ainsi que trois classes principales :
date
time
datetime
Vous vous en doutez, la classe date sert grer des dates, time grer du temps et datetime les deux.
Toutes ces classes disposent d'attributs permettant d'indiquer une anne, un mois, un jour, une heure et galement des informations de fuseau horaire.
Voyons dans le dtail comment crer ces objets :
>>> from datetime import date, time, datetime >>> date(year=2021, month=10, day=22) datetime.date(2021, 10, 22) >>> time(hour=10, minute=19, second=10) datetime.time(10, 19, 10) >>> datetime(year=2021, month=10, day=22, hour=10, minute=19, second=10) datetime.datetime(2021, 10, 22, 10, 19, 10)
Le nom du module datetime tant le mme que le nom de la classe, on confond souvent les deux. Si vous importez le module directement, il faudra donc utiliser datetime.datetime ce qui est assez confu. Je prfre ainsi importer directement la classe avec from datetime import datetime.
Remarquez que tous les noms des paramtres de ces classes sont au singulier (hour, day, minute...). Ce ne sera pas toujours le cas dans les diffrentes classes et fonctions que nous allons voir dans cet article, il est donc important de noter quand c'est le cas ou non.
Ces trois classes nous permettent de reprsenter des priodes dans le temps en utilisant des nombres entiers pour les annes, mois, jours, etc.
Les classes datetime et date disposent galement de deux fonctions, now et today qui permettent respectivement de rcuprer la date et l'heure d'aujourd'hui (pour datetime) et la date d'aujourd'hui (pour date) :
>>> datetime.now() datetime.datetime(2021, 10, 22, 10, 25, 7, 742193) >>> date.today() datetime.date(2021, 10, 22)
La fonction today est disponible sur les objets datetime mais la fonction now n'est pas disponible sur les objets date.
>>> datetime.today() datetime.datetime(2021, 10, 22, 10, 26, 36, 718675) >>> datetime.now() datetime.datetime(2021, 10, 22, 10, 26, 39, 182568) >>> date.now() Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: type object 'datetime.date' has no attribute 'now'
Une fois que nous avons cr un objet de time date, time ou datetime, nous pouvons rcuprer les diffrents attributs de ces objets :
>>> today = date.today() >>> today.day 22
L encore, notez l'utilisation du singulier pour le nom de l'attribut.
noter galement qu'il est possible de rcuprer les valeurs de ces attributs mais pas de les modifier directement.
>>> today = date.today() >>> today.day = 23 Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: attribute 'day' of 'datetime.date' objects is not writable
Pour remplacer un lment d'un objet datetime on peut utiliser la mthode replace :
>>> today = date.today() >>> tomorrow = today.replace(day=today.day + 1) >>> today datetime.date(2021, 10, 22) >>> tomorrow datetime.date(2021, 10, 23)
Je vous dconseille cependant d'utiliser cette mthode car elle ne prend pas en compte les problmes de fuseaux horaires et du passage l'heure d't. Nous verrons plus loin dans cet article comment modifier une date de la bonne faon.
Il arrive souvent que nous souhaitions crer un objet datetime non pas partir de nombres entiers mais directement partir d'une chane de caractres.
Le problme qui se pose est que la reprsentation des dates sous forme de chane de caractres peut prendre beaucoup de formes.
Par exemple aux tats-Unis, les dates sont gnralement reprsentes sous le format MM-DD-YYYY (par exemple 10-22-2021 pour reprsenter le 22 octobre 2021).
En Europe on utilise plutt DD-MM-YYYY (22-10-2021 pour le 22 octobre 2021).
Tout au long de cet article, je reprsenterai les jours, mois et annes avec les lettres D, M et Y (Day, Month et Year).
La norme ISO8601 a t cre pour pallier ce problme. Selon cette norme, on va de l'lment le plus grand au plus petit, une date serait donc reprsente par YYYY-MM-DD HH:MM:SS.
Les objets de la classe datetime permettent de crer une instance partir de ce format grce la fonction fromisoformat :
>>> from datetime import date
>>> date.fromisoformat("2021-10-22")
datetime.date(2021, 10, 22)
Dans le cas o une date n'est pas dans le format ISO8601 (c'est souvent le cas), on peut utiliser la place la fonction strptime qui permet de spcifier le formatage de la date.
Imaginons une date reprsente par la chane de caractres suivante :
'22 Oct 2021'
Grce strptime et un langage spcifique dont vous pouvez retrouver les caractristiques sur ce site, on peut indiquer le format de la date en le passant en 2e argument la fonction :
>>> datetime.strptime("22 Oct 2021", "%d %b %Y")
datetime.datetime(2021, 10, 22, 0, 0)
Cette mthode n'est disponible que sur la classe datetime. Si vous n'arrivez pas vous souvenir du nom de cette mthode, sachez que le p de strptime signifie parser ( analyseur en franais). strptime est donc un raccourci de string parser time.
Et si nous souhaitons raliser l'opration inverse, nous pouvons le faire avec la fonction strftime (string from time) :
>>> now = datetime.now()
>>> now.strftime("%d %b %Y")
'22 Oct 2021'
Deux modules non inclus dans la bibliothque standard de Python permettent de manipuler les dates avec beaucoup plus de facilit :
dateutil nous permet grce au module parser et la fonction parse de crer un objet datetime sans avoir besoin de spcifier le format comme nous l'avons fait avec strptime et avec beaucoup plus de liberts :
>>> from dateutil import parser
>>> parser.parse("12 october 2021 at 9 am and 18 minutes")
datetime.datetime(2021, 10, 12, 9, 18)
Notez cependant que nous utilisons ici october au lieu de octobre, cette bibliothque ne comprenant pas le franais :
>>> parser.parse("12 octobre 2021")
File "<stdin>", line 1, in <module>
raise ParserError("Unknown string format: %s", timestr)
dateutil.parser._parser.ParserError: Unknown string format: 12 octobre 2021
La bibliothque dateparser elle va cependant beaucoup plus loin en nous permettant d'utiliser des mots marqueurs de temps comme aujourd'hui ou demain et ce mme dans diffrentes langues () :
>>> dateparser.parse("aujourd'hui")
datetime.datetime(2021, 10, 22, 11, 11, 18, 244967)
>>> dateparser.parse("demain")
datetime.datetime(2021, 10, 23, 11, 10, 19, 246512)
>>> dateparser.parse("ontem") # La date d'hier en Portugais
datetime.datetime(2021, 10, 21, 11, 10, 46, 885066)
Cette bibliothque est vraiment incroyable et j'ai encore du mal trouver ses limites :
>>> dateparser.parse("Il y a un mois")
datetime.datetime(2021, 9, 22, 11, 12, 42, 170722)
>>> dateparser.parse("One year ago at midnight")
datetime.datetime(2020, 10, 22, 0, 0)
La gestion des fuseaux horaires est possible de diffrentes faons, surtout depuis l'ajout dans la version 3.9 de Python d'un nouveau module zoneinfo.
Les diffrents modules que nous allons voir dans cette partie utilisent une base de donnes appele tz database galement appele IANA database.
Vous pouvez retrouver tous les fuseaux horaires de cette base de donnes ainsi que leur nom sur la page wikipedia associe.
Une date naive est une date qui n'a aucune information de fuseau horaire (timezone en anglais). l'inverse une date aware est dfinie selon un fuseau horaire prcis.
Par dfaut, les objets datetime comme ceux que nous avons crs ci-dessus sont naive . La faon la plus simple de vrifier les informations de fuseau horaire d'un objet datetime est de vrifier l'attribut tzinfo :
>>> from datetime import datetime >>> now = datetime.now() >>> now.tzinfo None
Il existe diffrentes faons d'ajouter des informations de fuseau horaire un objet datetime en fonction des versions Python que vous utilisez.
Depuis la version 3.9 de Python, le module de la bibliothque standard zoneinfo permet de grer les fuseaux horaires. Si vous utilisez une version de Python infrieure 3.9, je vous conseille de regarder des modules comme dateutil ou pytz.
Les principes que nous allons voir dans la suite de cet article restent les mmes peu importe le module utilis.
Grce la classe ZoneInfo du module zoneinfo on peut crer un fuseau horaire en nous basant sur la base de donnes IANA.
Si je souhaite rcuprer le fuseau horaire de Vancouver, je peux utiliser la chane de caractres "America/Vancouver" :
>>> from zoneinfo import ZoneInfo
>>> now_in_vancouver = datetime.now(tz=ZoneInfo("America/Vancouver"))
>>> now_in_montreal = datetime.now(tz=ZoneInfo("America/Montreal"))
>>> now_in_montreal.hour
12
>>> now_in_vancouver.hour
9
On retrouve bien les 3 heures de dcalages entre Montral et Vancouver.
tant Montral l'heure o j'cris cet article, j'aurais pu utiliser datetime.now() sans prciser le paramtre tz pour rcuprer la date et l'heure de l'endroit o je me trouve.
J'aurais donc eu un objet aware (now_in_vancouver) et un objet naive (now_in_montreal).
Cela ne pose pas de problme tant que nous n'effectuons aucune opration entre ces deux objets. En cas de comparaison ou de soustraction par exemple, nous avons une erreur si nous utilisons un objet naive et un objet aware :
>>> now_in_vancouver = datetime.now(tz=ZoneInfo("America/Vancouver"))
>>> now_in_montreal = datetime.now()
>>> now_in_vancouver > now_in_montreal
File "<stdin>", line 1, in <module>
TypeError: cant compare offset-naive and offset-aware datetimes
>>> now_in_vancouver - now_in_montreal
File "<stdin>", line 1, in <module>
TypeError: cant subtract offset-naive and offset-aware datetimes
Pour changer de fuseau horaire, on utilise la mthode astimezone :
>>> now_in_paris = now_in_vancouver.astimezone(ZoneInfo("Europe/Paris"))
>>> print(now_in_paris.hour)
18
L encore, la diffrence est cohrente (9h de dcalage entre Vancouver et Paris, on passe donc de 9h 18h).
Vous avez dj probablement entendu les acronymes UTC ou GMT, souvent utiliss quand vous devez planifier une runion avec un collgue l'autre bout du monde.
On fait souvent la confusion entre UTC et un fuseau horaire.
UTC n'est pas un fuseau horaire, c'est une chelle de temps qui a t accepte par la grande majorit des pays comme chelle de temps universelle.
Petite anecdote intressante : l'appellation correcte en anglais du temps universel coordonn (TUC) serait coordinated universal time, abrg en CUT. Les experts de lUnion internationale des tlcommunications taient daccord pour dfinir une abrviation commune toutes les langues, mais ils taient diviss sur le choix de la langue entre le franais et l'anglais. Finalement, cest le compromis UTC, ncessitant un effort des deux parties, qui fut choisi. Cest cette notation qui est utilise par la norme ISO 8601. (de wikipedia)
On pense souvent tort qu'UTC correspond un fuseau horaire car nous pouvons convertir un object datetime en UTC grce au module timezone de la bibliothque datetime en le passant au paramtre tz :
>>> from datetime import datetime, timezone >>> now = datetime.now(tz=timezone.utc) >>> now datetime.datetime(2021, 10, 22, 15, 57, 4, 822209, tzinfo=datetime.timezone.utc) >>> now.tzinfo datetime.timezone.utc
Quand vous comparez des dates ou que vous souhaitez les modifier, il est trs important de toujours le faire sur une base UTC. Je rpte : ne faites pas d'oprations sur des dates avec un fuseau horaire spcifique.
Il est notamment dangereux d'effectuer une opration sur une date avec un fuseau horaire prcis car les changements d'heures t / hiver ne seront pas pris en compte.
Par exemple Montral, il existe deux fuseaux horaires, EDT et EST (Eastern Daylight Time et Estern Standard Time), selon les priodes de l'anne.
En 2020, le passage de l'heure d'hiver l'heure d't a eu lieu dans la nuit du 7 au 8 mars.
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>> montreal_tz = ZoneInfo('America/Montreal')
>>> print(datetime(2020, 3, 7, 13, 0, 0, tzinfo=montreal_tz))
2020-03-07 13:00:00-05:00
>>> print(datetime(2020, 3, 8, 13, 0, 0, tzinfo=montreal_tz))
2020-03-08 13:00:00-04:00
Dans le code ci-dessus, on peut voir la diffrence avec le temps UTC la fin de l'affichage des dates (2020-03-07 13:00:00-05:00)
Le 7 mars, on a une diffrence de 5h entre le fuseau horaire de Montral et UTC. Le 8 mars, la diffrence n'est plus que de 4h entre le fuseau horaire de Montral et UTC.
On peut galement voir cette diffrence avec le changement de nom du fuseau horaire :
>>> montreal_tz = ZoneInfo('America/Montreal')
>>> print(datetime(2020, 3, 7, 13, 0, 0, tzinfo=montreal_tz).tzname())
EST
>>> print(datetime(2020, 3, 8, 13, 0, 0, tzinfo=montreal_tz).tzname())
EDT
La diffrence d'une heure par rapport UTC est normale car on a avanc d'une heure dans la nuit du 7 au 8 mars 2020 Montral. L'cart relatif par rapport UTC n'est donc plus le mme entre le 7 et le 8 mars.
L'heure universelle UTC elle n'a pas boug, mais l'heure Montral oui. Et ces changements n'arrivent pas toujours au mme moment partout dans le monde.
C'est pour cette raison que tous les ans, pendant 3 semaines environ, il n'y a plus que 5h de dcalage entre Paris et Montral au lieu de 6 (car le passage l'heure d't en France se fait en gnral 2 3 semaines aprs le changement au Canada).
Et cette diffrence peut causer des problmes lorsque vous ralisez des oprations sur une date.
Pour tre trs prcis, EST et EDT ne sont pas des fuseaux horaires en soi. Un fuseau horaire est dfini par un continent et le plus souvent une ville, comme nous l'avons vu dans la base de donnes IANA (par exemple Europe/Paris ou America/Montreal).
EST et EDT reprsentent la diffrence de temps avec UTC. Quand nous indiquons un fuseau horaire ZoneInfo, nous donnons donc le nom du fuseau horaire (par exemple America/Montreal) et en fonction de la priode de l'anne, nous nous trouverons donc soit en EST soit en EDT, avec une diffrence par rapport UTC qui ne sera pas la mme.
D'ailleurs, pour afficher le ou les noms associs au fuseau horaire o l'on se trouve on peut utiliser le module time :
>>> import time
>>> time.tzname
('EST', 'EDT')
Voyons maintenant un exemple dans lequel nous travaillons directement avec un objet datetime sur le fuseau horaire de Montral.
Nous souhaitons ajouter un jour la date du 7 mars (nous utilisons pour ce faire la classe timedelta que nous verrons plus en dtail dans la suite de cet article) :
>>> from datetime import datetime, timedelta
>>> from zoneinfo import ZoneInfo
>>> montreal_tz = ZoneInfo('America/Montreal')
>>> march_7 = datetime(2020, 3, 7, 13, 0, 0, tzinfo=montreal_tz)
>>> march_8 = march_7 + timedelta(days=1)
>>> print(march_8)
2020-03-08 13:00:00-04:00
Remarquez que l'heure n'a pas boug. On vient juste d'ajouter 1 jour notre date, sans prendre en compte le passage l'heure d't.
Reprenons maintenant le mme exemple en convertissant d'abord notre date en UTC avant de faire l'addition, puis en modifiant le fuseau horaire par la suite :
>>> from datetime import datetime, timedelta
>>> from zoneinfo import ZoneInfo
>>> march_7 = datetime(2020, 3, 7, 13, 0, 0)
>>> march_7_utc = march_7.astimezone(ZoneInfo("UTC"))
>>> march_8 = march_7_utc + timedelta(days=1)
>>> march_8 = march_8.astimezone(montreal_tz)
>>> print(march_8)
2020-03-08 14:00:00-04:00
Nous remarquons cette fois-ci que l'heure d't a bien t prise en compte et notre date affiche 14h au lieu de 13.
En faisant l'opration sur une date base sur UTC puis en convertissant par la suite dans le fuseau horaire voulu, nous obtenons le bon rsultat.
Prenons un autre cas de figure pour illustrer les problmes causs lors du calcul de la diffrence entre deux dates :
>>> from datetime import datetime
>>> from zoneinfo import ZoneInfo
>>> utc = ZoneInfo("UTC")
>>> montreal_tz = ZoneInfo('America/Montreal')
>>> march_7 = datetime(2020, 3, 7, 13, 0, 0, tzinfo=montreal_tz)
>>> march_10 = datetime(2020, 3, 10, 13, 0, 0, tzinfo=montreal_tz)
>>> march_7_utc = march_7.astimezone(utc)
>>> march_10_utc = march_10.astimezone(utc)
>>> print(march_10 - march_7)
3 days, 0:00:00
>>> print(march_10_utc - march_7_utc)
2 days, 23:00:00
Dans le premier cas de figure (march_10 - march_7), une simple soustraction est effectue entre les deux dates au fuseau horaire de Montral. On obtient donc une diffrence parfaite de 3 jours qui ne prend l encore pas en compte le passage l'heure d't.
Dans le deuxime cas de figure, avec les dates converties en UTC, on observe cette fois-ci bien une diffrence d'1h avec le rsultat prcdant, indiquant la prise en compte de l'heure qui a t "perdue" lors du passage l'heure d't.
datetimeNous l'avons vu brivement dans la partie prcdente avec les timedelta, il est possible de raliser des oprations entre deux dates ou de modifier une date.
La classe timedelta permet d'ajouter un intervalle de temps une date. Attention, timedelta ne reprsente pas une date mais un intervalle. Vous remarquerez d'ailleurs que cette fois-ci, les attributs sont au pluriel :
>>> from datetime import timedelta >>> timedelta(days=20) datetime.timedelta(days=20)
La classe timedelta permet d'ajouter ou de soustraire des jours, heures, secondes et mme microsecondes. On peut sans problme utiliser des nombres positifs et ngatifs en mme temps.
Dans l'exemple ci-dessous, nous ajoutons 15 jours et soustrayons 5 heures la date actuelle :
>>> now = datetime.now() >>> now_in_15_days_minus_5_hours = now + timedelta(days=15, hours=-5) >>> print(now) 2021-10-22 17:43:23.840561 >>> print(now_in_15_days_minus_5_hours) 2021-11-06 12:43:23.840561
La date passe logiquement au mois suivant. C'est valable galement pour des mois comme le mois de fvrier qui ne contient que 28 jours.
>>> feb_27_2022 = datetime(2022, 2, 27) # 27 fvrier >>> print(feb_27_2022 + timedelta(days=3)) # On ajoute 3 jours 2022-03-02 00:00:00
On tombe bien sur le 2 mars et non le 30 fvrier.
La classe timedelta ne permet d'ajouter que des jours, heures, minutes mais pas des mois ou des annes. C'est cependant possible avec la classe relativedelta du module dateutil que nous avons vu prcdemment mais qui n'est pas disponible dans la bibliothque standard de Python :
>>> from dateutil.relativedelta import relativedelta >>> from datetime import datetime >>> now = datetime.now() >>> now_in_2_months = now + relativedelta(months=2) >>> print(now) 2021-10-22 17:52:20.878397 >>> print(now_in_2_months) 2021-12-22 17:52:20.878397
J'espre qu'avec cette partie vous y verrez un peu plus clair avec le module datetime. La base est assez simple matriser mais il est trs important dans un contexte professionnel de porter attention toutes les subtilits que nous avons vues dans cet article.
On peut penser que quelques heures de dcalage ne sont pas trs graves, mais a peut vite devenir problmatique quand vous souhaitez l'anniversaire vos clients un jour trop tt ou que vous envoyez un mail sur le mauvais fuseau horaire.
Pour un projet perso a peut passer, pour un projet avec des clients, il est trs important de porter attention ces dtails.